Skip to content

Conversation

@wonderwhy-er
Copy link
Owner

@wonderwhy-er wonderwhy-er commented Dec 1, 2025

User description

Summary

This PR improves PDF functionality with several enhancements:

Features

  • Page filtering - Read specific pages using offset/length parameters
  • Performance optimization - Optimized partial page reading
  • Image extraction - Extract and compress images from PDFs

Fixes

  • Fixed test for PDF modification (insert/delete/merge) - corrected API parameter order and schema property names

Commits

  • 3d0dff1 feat: add page filter, make output more comprehensive, extend tool descriptions
  • 84e796d feat: optimize performance for partial pages
  • 3f0ab32 fix: image extraction, compression
  • 72bca17 fix: remove png deps, adjust image optimization
  • ef9dbbc Tests fix (corrected writePdf test parameters)

Testing

  • test-pdf-parsing.js
  • test-pdf-creation.js

Summary by CodeRabbit

  • New Features

    • Create, edit, and export PDFs from Markdown; insert/delete/merge pages; parse PDFs to extract text, metadata, per-page images, and selected page ranges.
    • PDF read/write surfaced across file tools with clearer PDF guidance.
  • Tests

    • End-to-end scripts validating PDF creation, modification, and parsing workflows.
  • Chores

    • Added runtime dependencies to support PDF, Markdown, and image processing.

✏️ Tip: You can customize this high-level summary in your review settings.


CodeAnt-AI Description

Add structured PDF reading, image extraction, and page-level PDF editing

What Changed

  • Reading a PDF (from local path or URL) now auto-detects the file, extracts its text as page-structured markdown, includes embedded images, and supports page-based filtering using offset/length (including negative offsets from the end)
  • Reading multiple files now identifies PDFs as multi-page documents, summarizes them with page counts, and returns each page’s text and images alongside other file types
  • A new write_pdf tool lets users create PDFs from markdown and modify existing PDFs by inserting or deleting pages (from markdown or other PDFs), with responses that clearly show the original and new file locations
  • File information now treats PDFs as non-text (no line counts), and the regular file-writing tool explicitly warns not to use it for PDF creation, steering all PDF work through write_pdf
  • Added standalone test scripts that exercise PDF parsing (including page filtering and image extraction) and PDF creation/modification flows end to end

Impact

✅ Easier PDF content review
✅ Safer page-level PDF edits
✅ Richer PDF reading with embedded images

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Dec 1, 2025

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 1, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds end-to-end PDF support: new runtime deps; PDF↔Markdown conversion; image extraction/compression; PDF editing (insert/delete); filesystem and server integration (new write_pdf tool and handler); schemas and tests for creation and parsing.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Added PDF/Markdown and image-processing deps: @opendocsg/pdf2md, file-type, md-to-pdf, pdf-lib, remark, remark-gfm, remark-parse, sharp, unified, unpdf.
Server Integration
src/server.ts
Registered write_pdf tool with WritePdfArgsSchema, wired handleWritePdf in CallToolRequest, added flushDeferredMessages() export, and updated tool descriptions for PDF guidance.
Handler Layer
src/handlers/filesystem-handlers.ts
Added handleWritePdf(args: unknown) and enhanced read handlers to return PDF-aware payloads (per-page text, images, metadata) alongside existing flows.
Schemas
src/tools/schemas.ts
Added PdfInsertOperationSchema, PdfDeleteOperationSchema, PdfOperationSchema, and WritePdfArgsSchema (with content preprocessing).
PDF Tools — Core
src/tools/pdf/index.ts, src/tools/pdf/lib/pdf2md.ts, src/tools/pdf/markdown.ts
New PDF core: pdf2md parsing (metadata, per-page items, images), parsePdfToMarkdown, and parseMarkdownToPdf utilities with types.
PDF Tools — Manipulation
src/tools/pdf/manipulations.ts
Added editPdf() implementing insert/delete operations (insert from Markdown or source PDF, delete by normalized indexes).
PDF Tools — Images & Utils
src/tools/pdf/extract-images.ts, src/tools/pdf/utils.ts
New image extraction (batched, optional compression via dynamic sharp import) and page-index / page-range helpers (normalizePageIndexes, generatePageNumbers).
Filesystem Tools
src/tools/filesystem.ts
Added writePdf(); extended FileResult/MultiFileResult with isPdf and payload; integrated PDF detection/parsing into read flows.
MIME Type Detection
src/tools/mime-types.ts
Added application/pdf handling and isPdfFile(mimeType: string) utility.
Tests
test/test-pdf-creation.js, test/test-pdf-parsing.js
Added scripts to validate PDF creation/modification and PDF→Markdown parsing (page filtering and image extraction).
Cleanup
test/test_output/node_repl_debug.txt, test/test_output/repl_test_output.txt
Removed test output artifacts.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server
    participant Handler as FilesystemHandler
    participant FS as FilesystemTools
    participant PDF as PDFTools

    Client->>Server: CallToolRequest(write_pdf, args)
    Server->>Handler: dispatch to handleWritePdf(args)
    activate Handler

    alt create from Markdown
        Handler->>PDF: parseMarkdownToPdf(markdown)
        PDF-->>Handler: PDF buffer
        Handler->>FS: writePdf(path, buffer, options)
        FS-->>Handler: write result
    else edit existing PDF (operations)
        Handler->>FS: writePdf(path, operations, options)
        activate FS
        FS->>PDF: editPdf(path, operations)
        activate PDF
        loop operations
            alt insert
                PDF->>PDF: generate/insert pages (from markdown or source PDF)
            else delete
                PDF->>PDF: remove pages
            end
        end
        PDF-->>FS: modified PDF buffer
        FS-->>Handler: write result
        deactivate PDF
        deactivate FS
    end

    Handler-->>Server: success / error
    deactivate Handler
    Server-->>Client: CallToolResponse
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas needing extra attention:
    • src/tools/pdf/lib/pdf2md.ts — async parsing, metadata extraction, resource cleanup.
    • src/tools/pdf/extract-images.ts — batching, dynamic sharp import, conversion/error handling.
    • src/tools/pdf/manipulations.ts — page index normalization, insertion semantics, validation.
    • Integration surface: schemas → server → handler → filesystem tools → pdf utilities.

Possibly related PRs

Suggested reviewers

  • serg33v
  • dmitry-ottic-ai

Poem

🐰 I hopped through bytes and found the pages,

stitched text and images across the stages,
turned markdown into prints with glee,
compressed a carrot-sized image tree,
hop, patch, save — a rabbit-built PDF spree.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main changes: page filtering, image extraction, and performance optimization for PDF v3, which are the primary features implemented across the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-pdf-v3

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dcd643c and ba083c2.

📒 Files selected for processing (1)
  • src/server.ts (37 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/server.ts (1)
src/tools/schemas.ts (13)
  • WritePdfArgsSchema (78-99)
  • CreateDirectoryArgsSchema (101-103)
  • ListDirectoryArgsSchema (105-108)
  • MoveFileArgsSchema (110-113)
  • StartSearchArgsSchema (150-162)
  • GetMoreSearchResultsArgsSchema (164-168)
  • StopSearchArgsSchema (170-172)
  • ListSearchesArgsSchema (174-174)
  • GetFileInfoArgsSchema (115-117)
  • EditBlockArgsSchema (120-125)
  • GetUsageStatsArgsSchema (137-137)
  • GiveFeedbackArgsSchema (140-147)
  • GetPromptsArgsSchema (177-181)
🔇 Additional comments (8)
src/server.ts (8)

49-50: LGTM!

The WritePdfArgsSchema import is properly added alongside other schema imports and aligns with the schema definition in src/tools/schemas.ts.


71-76: LGTM!

The flushDeferredMessages() function is correctly implemented with proper array draining and the non-null assertion is safe given the while loop condition.


269-276: LGTM!

The PDF support documentation clearly explains the different behavior for offset and length parameters when reading PDFs (page-based vs line-based), which helps users understand the dual semantics.


307-311: LGTM!

Good defensive documentation warning users not to use write_file for PDFs and directing them to the appropriate write_pdf tool.


347-408: LGTM! Well-structured tool definition.

The write_pdf tool is thoroughly documented with:

  • Clear separation of CREATE and MODIFY modes
  • Strong guidance against overwriting originals (good for data safety)
  • Proper annotations including destructiveHint: true
  • Comprehensive examples for operations

The emphasis on using unique filenames and never overwriting originals is good practice for preventing accidental data loss.


1396-1409: LGTM!

The outer error handler properly captures errors and returns a safe error message without leaking internal details. The write_pdf case correctly relies on this outer handler, which is consistent with other filesystem tools.


182-232: Minor formatting changes look good.

The configuration tools maintain consistent structure with proper annotations. No functional changes in this section.


1248-1250: Handler routing looks correct.

The write_pdf case follows the same pattern as other filesystem handlers. The handleWritePdf function is properly exported from src/handlers/filesystem-handlers.ts and re-exported through src/handlers/index.ts, making it accessible to the handlers module used in server.ts.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codeant-ai codeant-ai bot added the size:XXL This PR changes 1000+ lines, ignoring generated files label Dec 1, 2025
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Dec 1, 2025

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • Possible Data Loss
    The new write_pdf tool description states that the original PDF must never be overwritten and that outputPath must always be a new filename, but the underlying implementation still allows outputPath to be omitted and will then overwrite the input file. This mismatch between documented behavior and actual behavior can lead to accidental data loss if callers follow the examples but the backend does not enforce these invariants. The reviewer should verify the writePdf handler and consider adding validation to prevent in-place modification when content is an operations array.

  • Breaking Change
    The new PDF handling branches for disk and URL now return FileResult.content as an empty string and put all useful data under payload.metadata and payload.pages, unlike other file types where content carries the main data. Existing tooling that assumes content is populated for all file types may now see empty content for PDFs; this change in contract should be validated against all consumers or a compatibility path should be provided.

  • Possible Bug
    The mapping between parsed pages and their corresponding page numbers (pagesToProcess vs pageNumberMap) is derived from two different sources. If pageNumbers is provided out of order (e.g. [5, 3]), the arrays can become misaligned, leading to incorrect pageNumber values and mismatched text/image associations for each page.

  • Possible Bug
    For PDFs, both readFileFromUrl and readFileFromDisk return FileResult objects with mimeType: 'text/plain' while also setting isPdf: true and moving the actual PDF-derived data into payload. This is inconsistent with the actual resource type and with helpers like isPdfFile (which work off MIME type) and may cause incorrect type detection or behavior in existing callers that rely on mimeType rather than isPdf.

  • Possible Bug
    The result of mdToPdf is used directly (pdf.content) without checking for undefined or an unexpected shape. According to the library API, mdToPdf can fail and return undefined, which would cause a runtime error when accessing content. This behavior and error handling should be validated.

  • Resource Cleanup
    The cleanup of pdfDocument is wrapped in a try/finally that only covers the final return. Any error thrown before that (e.g. during transformations or image extraction) will bypass the cleanup, potentially leaking resources held by the underlying PDF library.

  • Possible Bug
    In handleReadMultipleFiles, the detailed PDF handling is nested under a guard that checks result.content !== undefined. For some PDF implementations, structured data may live only in result.payload while result.content can be empty/undefined. In that case, PDF pages and images would be skipped from the response even though isPdf and payload.pages are available. This control-flow should be double-checked to ensure PDFs are always processed when payload is present, regardless of content.

  • Performance Issue
    In readFileFromUrl, PDFs trigger an initial fetch(url) (used only for status and headers) and then a second network call via parsePdfToMarkdown(url), which likely re-downloads the same resource and is not covered by the existing AbortController timeout. This can significantly increase latency and bandwidth usage for remote PDFs and bypasses the configured fetch timeout; it's worth confirming and refactoring to avoid double-fetching.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (9)
src/tools/schemas.ts (1)

78-99: Consider explicit type handling over string preprocessing.

The preprocessing logic attempts to auto-parse JSON strings into arrays, which can mask type errors. If a client incorrectly passes a stringified JSON array when they should pass a proper array, this silently accepts it rather than surfacing the type mismatch.

Consider one of these approaches:

Option 1: Remove preprocessing and require proper typing from callers:

-  content: z.preprocess(
-    (val) => {
-      // If it's a string that looks like JSON array, parse it
-      if (typeof val === 'string' && val.trim().startsWith('[')) {
-        try {
-          return JSON.parse(val);
-        } catch {
-          // If parsing fails, return as-is (might be markdown content)
-          return val;
-        }
-      }
-      // Otherwise return as-is
-      return val;
-    },
-    z.union([z.string(), z.array(PdfOperationSchema)])
-  ),
+  content: z.union([z.string(), z.array(PdfOperationSchema)]),

Option 2: If preprocessing is needed for a specific client, at least log parse failures:

      if (typeof val === 'string' && val.trim().startsWith('[')) {
        try {
          return JSON.parse(val);
        } catch {
-          // If parsing fails, return as-is (might be markdown content)
+          // If parsing fails, return as-is (might be markdown content)
+          console.warn('Failed to parse JSON array in WritePdfArgsSchema content');
          return val;
        }
      }
src/tools/mime-types.ts (1)

5-7: Consider consistent handling pattern for MIME types.

The PDF extension uses an early return while image types use a dictionary lookup. For consistency and maintainability, consider using the same pattern for all MIME type mappings.

Apply this diff for consistency:

 export function getMimeType(filePath: string): string {
   const extension = filePath.toLowerCase().split('.').pop() || '';
 
-  if (extension === "pdf") {
-    return "application/pdf";
-  }
-
   // Image types - only the formats we can display
   const imageTypes: Record<string, string> = {
     'png': 'image/png',
     'jpg': 'image/jpeg',
     'jpeg': 'image/jpeg',
     'gif': 'image/gif',
     'webp': 'image/webp'
   };
 
+  // Document types
+  const documentTypes: Record<string, string> = {
+    'pdf': 'application/pdf'
+  };
+
+  if (extension in documentTypes) {
+    return documentTypes[extension];
+  }
+
   // Check if the file is an image
   if (extension in imageTypes) {
     return imageTypes[extension];
   }
src/tools/pdf/markdown.ts (1)

9-16: Consider consistent return type for loadPdfToBuffer.

The function returns Buffer for local files and ArrayBuffer for URLs, requiring the caller to handle both. Consider normalizing to a single type (e.g., always return Uint8Array or Buffer).

Apply this diff to return consistent type:

-async function loadPdfToBuffer(source: string): Promise<Buffer | ArrayBuffer> {
+async function loadPdfToBuffer(source: string): Promise<Uint8Array> {
     if (isUrl(source)) {
         const response = await fetch(source);
-        return await response.arrayBuffer();
+        const arrayBuffer = await response.arrayBuffer();
+        return new Uint8Array(arrayBuffer);
     } else {
-        return await fs.readFile(source);
+        const buffer = await fs.readFile(source);
+        return new Uint8Array(buffer);
     }
 }

Then simplify the caller:

 export async function parsePdfToMarkdown(source: string, pageNumbers: number[] | PageRange = []): Promise<PdfParseResult> {
     try {
         const data = await loadPdfToBuffer(source);
 
-        // @ts-ignore: Type definition mismatch for ESM usage
-        return await pdf2md(new Uint8Array(data), pageNumbers);
+        return await pdf2md(data, pageNumbers);
 
     } catch (error) {
src/tools/filesystem.ts (4)

331-347: PDF handling from URL reads entire document regardless of offset/length.

The URL-based PDF reading (readFileFromUrl) doesn't support offset and length parameters for page filtering, unlike readFileFromDisk which passes these to parsePdfToMarkdown. This is inconsistent with the documented behavior that PDFs support page filtering.

Consider passing offset/length if the function signature is expanded, or document this limitation explicitly in the tool description.


1207-1214: Avoid mutating input operation objects directly.

The code mutates the original operation objects (o.sourcePdfPath = await validatePath(...)) which could cause unexpected side effects if the caller reuses the operations array. Consider creating new operation objects instead.

         for (const o of content) {
             if (o.type === 'insert') {
                 if (o.sourcePdfPath) {
-                    o.sourcePdfPath = await validatePath(o.sourcePdfPath);
+                    operations.push({
+                        ...o,
+                        sourcePdfPath: await validatePath(o.sourcePdfPath)
+                    });
+                    continue;
                 }
             }
             operations.push(o);
         }

1182-1183: Consider stronger typing for the options parameter.

The options parameter is typed as any, which bypasses TypeScript's type checking. Consider using a more specific type or at least Record<string, unknown> for better type safety.


915-922: Redundant isPdfFile check.

Line 915 calls isPdfFile(fileResult.mimeType) but the result is never used. The fileResult.isPdf property (line 921) is already set by readFile, making this check redundant.

-                const isPdf = isPdfFile(fileResult.mimeType);
                 return {
                     path: filePath,
                     content: typeof fileResult === 'string' ? fileResult : fileResult.content,
                     mimeType: typeof fileResult === 'string' ? "text/plain" : fileResult.mimeType,
                     isImage: typeof fileResult === 'string' ? false : fileResult.isImage,
                     isPdf: typeof fileResult === 'string' ? false : fileResult.isPdf,
                     payload: typeof fileResult === 'string' ? undefined : fileResult.payload
                 };
src/tools/pdf/manipulations.ts (1)

70-70: Use strict equality operator.

Line 70 uses == instead of === for the type check. For consistency with line 67 and JavaScript best practices, use strict equality.

-        else if (op.type == 'insert') {
+        else if (op.type === 'insert') {
src/tools/pdf/lib/pdf2md.ts (1)

107-122: Unusual try/finally pattern with return inside try.

The pattern of having return inside a try block with a finally for cleanup is functional (JavaScript guarantees finally runs), but it's unconventional and could confuse readers. A more common pattern:

-    try {
-        return { pages: processedPages, metadata };
-    } finally {
+    const result = { pages: processedPages, metadata };
+    
+    // Cleanup
+    try {
         if (pdfDocument) {
-            try {
-                if (typeof pdfDocument.cleanup === 'function') {
-                    await pdfDocument.cleanup(false);
-                }
-            } catch (e) { }
-            try {
-                if (typeof pdfDocument.destroy === 'function') {
-                    await pdfDocument.destroy();
-                }
-            } catch (e) { }
+            if (typeof pdfDocument.cleanup === 'function') {
+                await pdfDocument.cleanup(false);
+            }
+            if (typeof pdfDocument.destroy === 'function') {
+                await pdfDocument.destroy();
+            }
         }
-    }
+    } catch (e) { /* Ignore cleanup errors */ }
+    
+    return result;

However, the current code is functionally correct.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3f5824 and ef9dbbc.

⛔ Files ignored due to path filters (4)
  • package-lock.json is excluded by !**/package-lock.json
  • test/samples/01_sample_simple.pdf is excluded by !**/*.pdf
  • test/samples/02_sample_invoice.pdf is excluded by !**/*.pdf
  • test/samples/03_sample_compex.pdf is excluded by !**/*.pdf
📒 Files selected for processing (16)
  • package.json (1 hunks)
  • src/handlers/filesystem-handlers.ts (10 hunks)
  • src/server.ts (37 hunks)
  • src/tools/filesystem.ts (19 hunks)
  • src/tools/mime-types.ts (2 hunks)
  • src/tools/pdf/extract-images.ts (1 hunks)
  • src/tools/pdf/index.ts (1 hunks)
  • src/tools/pdf/lib/pdf2md.ts (1 hunks)
  • src/tools/pdf/manipulations.ts (1 hunks)
  • src/tools/pdf/markdown.ts (1 hunks)
  • src/tools/pdf/utils.ts (1 hunks)
  • src/tools/schemas.ts (1 hunks)
  • test/test-pdf-creation.js (1 hunks)
  • test/test-pdf-parsing.js (1 hunks)
  • test/test_output/node_repl_debug.txt (0 hunks)
  • test/test_output/repl_test_output.txt (0 hunks)
💤 Files with no reviewable changes (2)
  • test/test_output/repl_test_output.txt
  • test/test_output/node_repl_debug.txt
🧰 Additional context used
🧬 Code graph analysis (8)
test/test-pdf-parsing.js (3)
test/test-pdf-creation.js (2)
  • __filename (13-13)
  • markdown (31-58)
src/tools/pdf/index.ts (1)
  • parsePdfToMarkdown (3-3)
src/tools/pdf/markdown.ts (1)
  • parsePdfToMarkdown (27-38)
src/handlers/filesystem-handlers.ts (4)
src/tools/filesystem.ts (1)
  • writePdf (1178-1232)
src/types.ts (1)
  • ServerResult (73-77)
src/tools/schemas.ts (1)
  • WritePdfArgsSchema (78-99)
src/error-handlers.ts (1)
  • createErrorResponse (9-17)
src/tools/pdf/manipulations.ts (4)
src/tools/pdf/index.ts (5)
  • PdfInsertOperation (2-2)
  • PdfDeleteOperation (2-2)
  • PdfOperations (2-2)
  • editPdf (1-1)
  • parseMarkdownToPdf (3-3)
src/tools/schemas.ts (3)
  • PdfInsertOperationSchema (63-69)
  • PdfDeleteOperationSchema (71-74)
  • PdfOperationSchema (76-76)
src/tools/pdf/utils.ts (1)
  • normalizePageIndexes (5-12)
src/tools/pdf/markdown.ts (1)
  • parseMarkdownToPdf (40-50)
src/tools/pdf/extract-images.ts (1)
src/tools/pdf/index.ts (3)
  • ImageInfo (6-6)
  • PageImages (6-6)
  • extractImagesFromPdf (5-5)
src/tools/pdf/markdown.ts (2)
src/tools/pdf/lib/pdf2md.ts (3)
  • PageRange (59-62)
  • PdfParseResult (35-38)
  • pdf2md (70-123)
src/tools/pdf/index.ts (2)
  • parsePdfToMarkdown (3-3)
  • parseMarkdownToPdf (3-3)
src/tools/pdf/lib/pdf2md.ts (2)
src/tools/pdf/extract-images.ts (2)
  • ImageInfo (3-16)
  • extractImagesFromPdf (39-132)
src/tools/pdf/utils.ts (1)
  • generatePageNumbers (21-44)
test/test-pdf-creation.js (1)
src/tools/filesystem.ts (1)
  • writePdf (1178-1232)
src/server.ts (1)
src/tools/schemas.ts (26)
  • GetConfigArgsSchema (4-4)
  • SetConfigValueArgsSchema (6-15)
  • ReadFileArgsSchema (45-50)
  • ReadMultipleFilesArgsSchema (52-54)
  • WriteFileArgsSchema (56-60)
  • WritePdfArgsSchema (78-99)
  • CreateDirectoryArgsSchema (101-103)
  • ListDirectoryArgsSchema (105-108)
  • MoveFileArgsSchema (110-113)
  • StartSearchArgsSchema (150-162)
  • GetMoreSearchResultsArgsSchema (164-168)
  • StopSearchArgsSchema (170-172)
  • ListSearchesArgsSchema (174-174)
  • GetFileInfoArgsSchema (115-117)
  • EditBlockArgsSchema (120-125)
  • StartProcessArgsSchema (21-26)
  • ReadProcessOutputArgsSchema (28-32)
  • InteractWithProcessArgsSchema (128-134)
  • ForceTerminateArgsSchema (34-36)
  • ListSessionsArgsSchema (38-38)
  • ListProcessesArgsSchema (18-18)
  • KillProcessArgsSchema (40-42)
  • GetUsageStatsArgsSchema (137-137)
  • GetRecentToolCallsArgsSchema (184-188)
  • GiveFeedbackArgsSchema (140-147)
  • GetPromptsArgsSchema (177-181)
🪛 Biome (2.1.2)
src/tools/pdf/extract-images.ts

[error] 162-162: eval() exposes to security risks and performance issues.

See the MDN web docs for more details.
Refactor the code so that it doesn't need to call eval().

(lint/security/noGlobalEval)

🔇 Additional comments (16)
src/tools/mime-types.ts (1)

27-29: LGTM!

The isPdfFile helper is clean and correct.

test/test-pdf-creation.js (2)

20-146: LGTM!

The test structure is well-organized with:

  • Clear documentation of each operation
  • Sequential state tracking in comments
  • Proper error handling and verification
  • Cleanup of temporary files

94-106: Negative index support is properly implemented and documented.

The normalizePageIndexes utility function in src/tools/pdf/utils.ts correctly handles negative indices by converting them to absolute positions using the formula pageCount + idx. For the test case with pageIndexes: [-1], a 2-page document will correctly resolve to index 1 (the last page), matching the test's sequential expectations. The implementation is already in place and the test is valid.

test/test-pdf-parsing.js (1)

28-93: LGTM!

The test provides comprehensive coverage:

  • Content extraction and preview
  • Image extraction and file saving
  • Multiple sample PDFs and URL testing
  • Good error handling

Also applies to: 145-163

src/handlers/filesystem-handlers.ts (2)

69-95: LGTM!

The PDF handling logic properly:

  • Extracts metadata (author, title, page count)
  • Flattens pages and images into content items
  • Adds page markers for context

144-175: LGTM!

The multi-file PDF handling correctly:

  • Summarizes PDF files with page counts
  • Includes all page content and images
  • Maintains proper structure
src/tools/pdf/utils.ts (2)

5-12: LGTM!

The normalizePageIndexes function correctly:

  • Handles negative indices (counting from end)
  • Filters to valid range [0, pageCount-1]
  • Removes duplicates

21-44: The generatePageNumbers function handles all edge cases correctly. Testing confirms:

  • offset=0, length=0 returns []
  • offset=-100, totalPages=10 correctly clamps to start at page 1
  • Out-of-bounds offsets are properly handled via early returns and Math.max/Math.min clamping

No issues found. The function's edge case handling is sound.

src/tools/pdf/markdown.ts (2)

40-50: LGTM!

The parseMarkdownToPdf function correctly:

  • Passes markdown as content to md-to-pdf
  • Returns the PDF buffer
  • Handles errors appropriately

31-32: Clarify the @ts-ignore comment with the specific type resolution issue.

The comment "Type definition mismatch for ESM usage" is too vague. The library has TypeScript declarations but they may not be properly exposed in the published package.json or there's an ESM/CJS export resolution conflict. Replace the comment with a more specific explanation of why the type assertion is necessary (e.g., "Types not properly resolved due to ESM/CJS export mismatch" or reference the specific TypeScript error encountered).

src/tools/pdf/index.ts (1)

1-6: LGTM!

Clean barrel file pattern that provides a centralized public API for the PDF module. The exports are well-organized by functionality (editing, markdown conversion, metadata types, and image extraction).

src/server.ts (2)

347-394: LGTM!

The write_pdf tool definition is comprehensive with:

  • Clear distinction between CREATE and MODIFY modes
  • Explicit rules about filenames and outputPath requirements
  • Well-documented operation types (delete, insert)
  • Appropriate annotations for destructive hints

The integration follows the established pattern of other tools in the server.


1234-1236: LGTM!

The handler case for write_pdf follows the established pattern and properly delegates to handlers.handleWritePdf.

src/tools/pdf/manipulations.ts (1)

61-87: LGTM with minor suggestion above.

The editPdf function is well-structured:

  • Properly loads PDF from path or buffer
  • Handles both delete and insert operations
  • Good error handling for missing insert sources
  • Uses type inference from Zod schemas for type safety
src/tools/pdf/extract-images.ts (1)

117-129: LGTM!

The cleanup logic in the finally block is well-implemented with proper error handling. Checking for function existence before calling and silently catching cleanup errors is appropriate for resource cleanup.

src/tools/pdf/lib/pdf2md.ts (1)

70-103: LGTM!

The pdf2md function is well-implemented:

  • Proper handling of both explicit page numbers array and PageRange object
  • Correct page number mapping for filtered pages
  • Integration with image extraction per page
  • Clean metadata extraction

The logic for calculating which pages to process and maintaining the page number mapping is correct.

data: compressionResult.data,
mimeType: compressionResult.mimeType,
originalSize,
compressedSize: Math.round(compressionResult.data.length * 0.75) // Approximate base64 overhead
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incorrect base64 overhead calculation.

The compressedSize calculation multiplies by 0.75, but base64 encoding increases size by approximately 33% (ratio of 4:3), so you should multiply by approximately 0.75 to get the original bytes from base64, not the other way around.

The comment says "Approximate base64 overhead" but the calculation gives the decoded size, not the encoded size. The data.length is already the base64 string length. If you want the approximate original binary size:

-                                    compressedSize: Math.round(compressionResult.data.length * 0.75) // Approximate base64 overhead
+                                    compressedSize: compressionResult.data.length // Base64 encoded size

Or if you want the actual binary size before base64 encoding:

-                                    compressedSize: Math.round(compressionResult.data.length * 0.75) // Approximate base64 overhead
+                                    compressedSize: Math.ceil(compressionResult.data.length * 3 / 4) // Approximate binary size from base64
🤖 Prompt for AI Agents
In src/tools/pdf/extract-images.ts around line 93, the compressedSize
calculation uses Math.round(compressionResult.data.length * 0.75) while the
comment says "Approximate base64 overhead" — this is inconsistent: if
compressionResult.data.length is the base64 string length and you intend
compressedSize to represent the original binary size before base64, keep the
multiplication by 3/4 (0.75) but update the comment to "Approximate original
binary size decoded from base64"; if instead you intended compressedSize to
represent the encoded (base64) size from original bytes, change the calculation
to Math.round(originalByteLength * 4 / 3) (or multiply by ~1.333) and update the
comment to "Approximate base64 encoded size (4:3 overhead)" so the math and
comment match.

pageIndex: z.number(),
markdown: z.string().optional(),
sourcePdfPath: z.string().optional(),
pdfOptions: z.object({}).passthrough().optional(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how pdfOptions and options are used in the schema context

echo "=== File structure and schema definitions ==="
head -120 src/tools/schemas.ts | tail -60

echo ""
echo "=== Searching for pdfOptions usage ==="
rg -nP --type=ts -C3 '\bpdfOptions\b'

echo ""
echo "=== Searching for schema field access patterns ==="
rg -nP --type=ts -C3 'parsed\.(options|pdfOptions)'

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 2938


🏁 Script executed:

#!/bin/bash
# Find the writePdf function implementation and check option handling

echo "=== Looking for writePdf function definition ==="
rg -nP --type=ts -A15 'function writePdf|const writePdf'

echo ""
echo "=== Checking if pdfOptions is used anywhere (excluding schema) ==="
rg -nP --type=ts 'pdfOptions' | grep -v 'z.object'

echo ""
echo "=== Checking how operations are processed in writePdf context ==="
rg -nP --type=ts -C5 'PdfInsertOperationSchema|pdfOptions' src/handlers/

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 1228


🏁 Script executed:

#!/bin/bash
# Get the full writePdf implementation

echo "=== Full writePdf function ==="
sed -n '1178,1300p' src/tools/filesystem.ts

echo ""
echo "=== Check if md-to-pdf is used ==="
rg -nP --type=ts 'md-to-pdf|mdtopdf' -C3

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 2875


🏁 Script executed:

#!/bin/bash
# Check parseMarkdownToPdf implementation

echo "=== parseMarkdownToPdf function ==="
rg -nP --type=ts -A20 'function parseMarkdownToPdf|const parseMarkdownToPdf'

echo ""
echo "=== Check the full markdown.ts file context ==="
head -50 src/tools/pdf/markdown.ts

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 2153


Review unvalidated options passed to external libraries and remove unused schema field.

The options field uses .passthrough() and is passed directly to mdToPdf at src/tools/pdf/markdown.ts:42 without any validation. Consider validating these options against a whitelist of allowed properties before passing them to the external library.

Additionally, pdfOptions in PdfInsertOperationSchema (line 68) is defined but never used anywhere in the codebase—it's not referenced in the editPdf function or any PDF operation handlers. Remove this unused field from the schema.

🤖 Prompt for AI Agents
In src/tools/schemas.ts around line 68 remove the unused pdfOptions field from
PdfInsertOperationSchema (it is not referenced anywhere) and replace the current
options: z.object({}).passthrough().optional() with a constrained schema that
validates only a whitelist of allowed mdToPdf options (declare explicit
keys/types and mark others unknown as forbidden); then update callers to use the
validated value. Specifically, remove pdfOptions, create a new OptionsSchema
listing only the permitted properties, change the options schema to that
OptionsSchema.optional(), and in src/tools/pdf/markdown.ts (around line 42)
validate/parse the options with this schema before passing them into mdToPdf so
only allowed keys are forwarded.

const SAMPLES = [
'01_sample_simple.pdf',
'02_sample_invoice.pdf',
'03_sample_compex.pdf',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix filename typo.

The filename '03_sample_compex.pdf' appears to have a typo. Should it be '03_sample_complex.pdf'?

Apply this diff if it's a typo:

-    '03_sample_compex.pdf',
+    '03_sample_complex.pdf',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'03_sample_compex.pdf',
'03_sample_complex.pdf',
🤖 Prompt for AI Agents
In test/test-pdf-parsing.js around line 20, the filename '03_sample_compex.pdf'
contains a typo; replace it with the correct filename '03_sample_complex.pdf' so
the test references the intended file, and run the tests to confirm the
corrected path resolves successfully.

}
}

if (import.meta.url === `file://${process.argv[1]}`) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The condition that detects whether the script is being run directly compares import.meta.url to a string built from process.argv[1], which is unreliable because process.argv[1] may be a relative path or use platform-specific separators (especially on Windows), so main() will often never be invoked even when the file is executed with node test/test-pdf-creation.js; comparing normalized filesystem paths instead makes the entrypoint detection work correctly across environments. [logic error]

Severity Level: Minor ⚠️

Suggested change
if (import.meta.url === `file://${process.argv[1]}`) {
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
Why it matters? ⭐

The current entrypoint check is brittle and often wrong. import.meta.url
is a file:// URL with an absolute, normalized path, while
process.argv[1] is whatever the user typed (node test/test-pdf-creation.js,
node .\test\test-pdf-creation.js, etc.). That means
file://${process.argv[1]} usually does not equal import.meta.url,
especially with relative paths or Windows backslashes, so main() will
frequently never run even when the script is invoked directly.

The proposed check compares normalized filesystem paths:
path.resolve(process.argv[1]) === __filename, where __filename is already
derived from import.meta.url via fileURLToPath. That's the standard ESM
pattern and actually detects "run as main module" across platforms.

I confirmed via search that this import.meta.url === \file://${process.argv[1]}``
idiom is copy‑pasted in other tests in this repo, but repetition of a bad
pattern doesn't make it correct. Here it's a real logic bug in the new file,
and the improved code fixes it without changing any other behavior.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** test/test-pdf-creation.js
**Line:** 148:148
**Comment:**
	*Logic Error: The condition that detects whether the script is being run directly compares `import.meta.url` to a string built from `process.argv[1]`, which is unreliable because `process.argv[1]` may be a relative path or use platform-specific separators (especially on Windows), so `main()` will often never be invoked even when the file is executed with `node test/test-pdf-creation.js`; comparing normalized filesystem paths instead makes the entrypoint detection work correctly across environments.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

await testSample('URL Sample', URL_SAMPLE);
}

if (import.meta.url === `file://${process.argv[1]}`) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The entrypoint guard compares import.meta.url directly to file://${process.argv[1]}, which fails when the script is invoked with a relative path (a common case), so main() will not run even though the file is being executed directly; normalizing the argv path before comparison fixes this. [logic error]

Severity Level: Minor ⚠️

Suggested change
if (import.meta.url === `file://${process.argv[1]}`) {
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
Why it matters? ⭐

The current guard compares import.meta.url (a file:///abs/path/...
URL) to file://${process.argv[1]}. When you run node test/test-pdf-parsing.js from the repo root, process.argv[1] is
test/test-pdf-parsing.js, so the comparison is file:///abs/path/...
vs file://test/test-pdf-parsing.js — they never match and main() never
runs. That's a real logic bug in how the script decides it's the entry
point.

The suggested fix:

if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
    main().catch(console.error);
}

uses path.resolve(process.argv[1]) and the already-defined __filename
(derived from import.meta.url) to compare absolute paths, which works
correctly for both relative and absolute invocations, and avoids running
main() when the file is merely imported. This is a bona fide logic fix,
not just cosmetic behavior.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** test/test-pdf-parsing.js
**Line:** 161:161
**Comment:**
	*Logic Error: The entrypoint guard compares `import.meta.url` directly to `file://${process.argv[1]}`, which fails when the script is invoked with a relative path (a common case), so `main()` will not run even though the file is being executed directly; normalizing the argv path before comparison fixes this.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment on lines +65 to +73
pageIndex: z.number(),
markdown: z.string().optional(),
sourcePdfPath: z.string().optional(),
pdfOptions: z.object({}).passthrough().optional(),
});

export const PdfDeleteOperationSchema = z.object({
type: z.literal('delete'),
pageIndexes: z.array(z.number()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The schemas for PDF page operations allow any numeric value for page indexes, including non-integers, which will be passed directly into pdf-lib's removePage/insertPage and can cause runtime errors or misaligned pages when a non-integer is accidentally provided. [logic error]

Severity Level: Minor ⚠️

Suggested change
pageIndex: z.number(),
markdown: z.string().optional(),
sourcePdfPath: z.string().optional(),
pdfOptions: z.object({}).passthrough().optional(),
});
export const PdfDeleteOperationSchema = z.object({
type: z.literal('delete'),
pageIndexes: z.array(z.number()),
pageIndex: z.number().int(),
markdown: z.string().optional(),
sourcePdfPath: z.string().optional(),
pdfOptions: z.object({}).passthrough().optional(),
});
export const PdfDeleteOperationSchema = z.object({
type: z.literal('delete'),
pageIndexes: z.array(z.number().int()),
Why it matters? ⭐

The implementation in src/tools/pdf/manipulations.ts treats page indices as
discrete positions into the PDF (including negative indices meaning "from the
end"), and passes them straight into PDFDocument.removePage/insertPage.
Conceptually these are integer indices; accepting fractional numbers in the
schema is a type/validation mismatch with the domain and can lead to
nonsensical values being forwarded to pdf-lib. Tightening the Zod schemas to
z.number().int() for pageIndex and pageIndexes moves that constraint
into validation, preventing callers from sending invalid non-integer indices
in the first place. The change is syntactically correct, local to the
schemas, and aligns the API with the actual expectations of the PDF logic,
so it guards against a real class of runtime errors instead of being
purely cosmetic.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/tools/schemas.ts
**Line:** 65:73
**Comment:**
	*Logic Error: The schemas for PDF page operations allow any numeric value for page indexes, including non-integers, which will be passed directly into `pdf-lib`'s `removePage`/`insertPage` and can cause runtime errors or misaligned pages when a non-integer is accidentally provided.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

Comment on lines +33 to +40
if (startPage > totalPages) return [];
const safeStart = Math.max(1, startPage);

// Compute final page (inclusive), truncated by totalPages
const endPage = Math.min(safeStart + length - 1, totalPages);

const count = endPage - safeStart + 1;
if (count <= 0) return [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: When an invalid or out-of-range page range is requested (e.g., offset beyond the document length or non-positive length), generatePageNumbers currently returns an empty array, but downstream code in the PDF-to-Markdown pipeline treats an empty array as "no filter provided" and processes all pages, so the user's filter is silently ignored instead of yielding no pages; returning a sentinel value that cannot match any real page ensures the filter produces zero pages instead of expanding to the full document. [logic error]

Severity Level: Minor ⚠️

Suggested change
if (startPage > totalPages) return [];
const safeStart = Math.max(1, startPage);
// Compute final page (inclusive), truncated by totalPages
const endPage = Math.min(safeStart + length - 1, totalPages);
const count = endPage - safeStart + 1;
if (count <= 0) return [];
if (startPage > totalPages) return [-1];
const safeStart = Math.max(1, startPage);
// Compute final page (inclusive), truncated by totalPages
const endPage = Math.min(safeStart + length - 1, totalPages);
const count = endPage - safeStart + 1;
// Return a sentinel page number (outside the valid 1..totalPages range)
// so downstream filters treat this as "some filter but no matching pages"
if (count <= 0) return [-1];
Why it matters? ⭐

This is a real logic bug, not cosmetic. In pdf2md, an empty
filterPageNumbers array is explicitly treated as "no filter" and
causes all pages to be processed:

const filterPageNumbers = Array.isArray(pageNumbers)
? pageNumbers
: generatePageNumbers(...);

const pagesToProcess = filterPageNumbers.length === 0
? pages
: pages.filter((_, index) => filterPageNumbers.includes(index + 1));

So when generatePageNumbers returns [] for an out-of-range or
non-positive-length range, the caller silently expands to the full
document instead of yielding no pages. Returning a sentinel like [-1]
keeps filterPageNumbers.length !== 0, so the filtering branch runs,
but index + 1 never equals -1, resulting in zero pages processed,
which matches the user's intent for an invalid range. The sentinel is
also stripped in pageNumberMap (filter(pageNum => pageNum >= 1 && pageNum <= pages.length)), and extractImagesFromPdf treats an empty
pageNumbers array as "no pages" (because [] is truthy and no
fallback to "all pages" occurs there), so this change stops the
incorrect "expand to all pages" behavior without breaking image
extraction. Net result: a concrete logic error is fixed.

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/tools/pdf/utils.ts
**Line:** 33:40
**Comment:**
	*Logic Error: When an invalid or out-of-range page range is requested (e.g., offset beyond the document length or non-positive length), `generatePageNumbers` currently returns an empty array, but downstream code in the PDF-to-Markdown pipeline treats an empty array as "no filter provided" and processes all pages, so the user's filter is silently ignored instead of yielding no pages; returning a sentinel value that cannot match any real page ensures the filter produces zero pages instead of expanding to the full document.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Dec 1, 2025

CodeAnt AI finished reviewing your PR.

wonderwhy-er and others added 5 commits December 1, 2025 13:41
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: codeant-ai[bot] <151821869+codeant-ai[bot]@users.noreply.github.com>
Co-authored-by: codeant-ai[bot] <151821869+codeant-ai[bot]@users.noreply.github.com>
Co-authored-by: codeant-ai[bot] <151821869+codeant-ai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/handlers/filesystem-handlers.ts (1)

203-227: Handle potential missing config consistently with handleReadFile.

Here you assume configManager.getConfig() always returns a non-null value:

const config = await configManager.getConfig();
const MAX_LINES = config.fileWriteLineLimit ?? 50;

In handleReadFile you explicitly guard against !config and return a structured error instead. To avoid a possible Cannot read properties of undefined and to keep behavior consistent, consider:

const config = await configManager.getConfig();
if (!config) {
  return createErrorResponse('Configuration not available');
}
const MAX_LINES = config.fileWriteLineLimit ?? 50;

This keeps the rest of the handler unchanged while avoiding an avoidable runtime crash path.

♻️ Duplicate comments (2)
src/tools/pdf/extract-images.ts (1)

93-93: Misleading comment on compressedSize calculation.

The calculation Math.round(compressionResult.data.length * 0.75) correctly estimates the binary size from the base64 string, but the comment "Approximate base64 overhead" is misleading. This is actually computing the approximate original binary size, not the overhead.

-                                    compressedSize: Math.round(compressionResult.data.length * 0.75) // Approximate base64 overhead
+                                    compressedSize: Math.ceil(compressionResult.data.length * 3 / 4) // Approximate binary size from base64
src/handlers/filesystem-handlers.ts (1)

56-67: Configured default read limit may still be ignored if Zod sets a length default.

You derive defaultLimit from config.fileReadLineLimit, but then always use parsed.length ?? defaultLimit. If ReadFileArgsSchema still defines length with a Zod .default(...), parsed.length will always be set and the config-driven limit will never be used, which is the same issue flagged in the earlier review.

Consider detecting whether length was actually provided on the raw args and only using parsed.length in that case; otherwise, fall back to the config limit:

const rawArgs = args as { offset?: number; length?: number } | undefined;

// Use caller‑provided values when present; otherwise, fall back to config defaults.
const offset = rawArgs && 'offset' in rawArgs ? parsed.offset : 0;
const length = rawArgs && 'length' in rawArgs ? parsed.length : defaultLimit;

This keeps the nice Zod parsing while making fileReadLineLimit effective again.

🧹 Nitpick comments (1)
src/tools/pdf/extract-images.ts (1)

170-170: Consider validating channels before type assertion.

The type assertion channels as 1 | 2 | 3 | 4 assumes unpdf always returns a valid channel count. If unpdf returns an unexpected value (e.g., 0 or 5), sharp may throw a cryptic error. Since sharp's error handling is already in a try/catch, this is low risk, but a validation could provide clearer diagnostics.

+        // Validate channel count for sharp compatibility
+        if (channels < 1 || channels > 4) {
+            console.warn(`Unsupported channel count: ${channels}`);
+            return null;
+        }
+
         let pipeline = sharp(Buffer.from(data), {
             raw: {
                 width,
                 height,
                 channels: channels as 1 | 2 | 3 | 4
             }
         });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef9dbbc and 0906011.

📒 Files selected for processing (2)
  • src/handlers/filesystem-handlers.ts (10 hunks)
  • src/tools/pdf/extract-images.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/handlers/filesystem-handlers.ts (4)
src/tools/filesystem.ts (1)
  • writePdf (1178-1232)
src/types.ts (1)
  • ServerResult (73-77)
src/tools/schemas.ts (1)
  • WritePdfArgsSchema (78-99)
src/error-handlers.ts (1)
  • createErrorResponse (9-17)
🔇 Additional comments (10)
src/tools/pdf/extract-images.ts (5)

1-31: LGTM!

The interfaces are well-defined with clear JSDoc documentation. The type definitions provide good contracts for image extraction and compression options.


50-56: LGTM!

Good use of batch processing to manage memory and avoid overwhelming the system when processing large PDFs. The batch size of 5 is a reasonable default.


117-129: LGTM!

Proper resource cleanup in the finally block with defensive checks for method existence. This ensures the PDF document is always cleaned up regardless of success or failure.


160-172: Dynamic import fix looks good.

The previous eval('import("sharp")') pattern has been replaced with proper dynamic import syntax. This resolves the security concern.


178-193: LGTM!

Clean format handling with webp as the sensible default for better compression. The mimeType is correctly set based on the output format.

src/handlers/filesystem-handlers.ts (5)

69-95: PDF branch in handleReadFile looks solid and robustly structured.

The PDF handling here (metadata header plus per‑page images followed by annotated page text) is consistent and uses optional chaining around pages and images, which should avoid crashes when images are absent. The content structure also looks friendly for downstream consumers.


119-130: Clarify withTimeout behavior and the null fallback.

You pass null as the fallback value to withTimeout but then treat result == null as an “impossible case” and throw a generic error. If withTimeout can resolve with the fallback on timeout, this path will be hit in real timeouts and produce an unstructured error instead of a ServerResult.

Please double‑check withTimeout’s contract and either:

  • Remove the fallback argument if timeouts always throw, or
  • Treat result == null as a timeout case and return a proper ServerResult (e.g., via createErrorResponse('Read file handler operation timed out')).

221-227: Success message and performance tip composition looks fine.

The dynamic modeMessage and appended performance tip (for large writes) read correctly and won’t be emitted unless writeFile succeeds, which matches the messaging. Minor duplication of “Successfully … File written successfully” is acceptable given the current UX goal.


296-301: get_file_info response formatting is clear and compact.

The Object.entries(info)...join('\n') approach produces a simple, line-oriented summary that’s easy for tools and humans to parse. No issues here.


314-326: handleWritePdf wiring and messaging look correct.

Argument parsing, delegation to writePdf, and the success message (with proper fallback to parsed.path when outputPath is omitted) are all consistent and error-safe. The error path correctly uses createErrorResponse.

Comment on lines 112 to +176
// Create a text summary of all files
const textSummary = fileResults.map(result => {
if (result.error) {
return `${result.path}: Error - ${result.error}`;
} else if (result.isPdf) {
return `${result.path}: PDF file with ${result.payload?.pages?.length} pages`;
} else if (result.mimeType) {
return `${result.path}: ${result.mimeType} ${result.isImage ? '(image)' : '(text)'}`;
} else {
return `${result.path}: Unknown type`;
}
}).join("\n");

// Create content items for each file
const contentItems: Array<{type: string, text?: string, data?: string, mimeType?: string}> = [];
const contentItems: Array<{ type: string, text?: string, data?: string, mimeType?: string }> = [];

// Add the text summary
contentItems.push({ type: "text", text: textSummary });

// Add each file content
for (const result of fileResults) {
if (!result.error && result.content !== undefined) {
if (result.isImage && result.mimeType) {
if (result.isPdf) {
result.payload?.pages.forEach((page, i) => {
page.images.forEach((image, i) => {
contentItems.push({
type: "image",
data: image.data,
mimeType: image.mimeType
});
});
contentItems.push({
type: "text",
text: page.text,
});
});
} else if (result.isImage && result.mimeType) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/handlers/filesystem-handlers.ts | head -200 | tail -70

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 3028


🏁 Script executed:

# Check file size first
wc -l src/handlers/filesystem-handlers.ts

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 115


🏁 Script executed:

# Look for type definitions
fd -e ts | xargs grep -l "interface.*Result\|type.*Result" | head -5

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 198


🏁 Script executed:

# Search for where payload is assigned/used
rg "payload\s*=" src/handlers/filesystem-handlers.ts -B 2 -A 2

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 58


🏁 Script executed:

# Check type definitions
cat -n src/types.ts

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 2365


🏁 Script executed:

# Find readMultipleFiles function definition
rg "function readMultipleFiles|const readMultipleFiles" src/ -B 2 -A 15

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 1448


🏁 Script executed:

# Find MultiFileResult type definition
rg "interface MultiFileResult|type MultiFileResult" src/ -B 2 -A 10

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 1521


🏁 Script executed:

# Look for payload structure definition
rg "payload.*=" src/tools/filesystem.ts -B 5 -A 5

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 777


🏁 Script executed:

# Find FileResultPayloads type definition
rg "interface FileResultPayloads|type FileResultPayloads" src/ -B 2 -A 15

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 860


🏁 Script executed:

# Search for where payload is created (PDF parsing)
rg "pages:\s*\[|images:\s*\[" src/ -B 5 -A 2 | head -60

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 1264


🏁 Script executed:

# Find PdfPayload type definition
rg "interface PdfPayload|type PdfPayload" src/ -B 2 -A 10

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 602


🏁 Script executed:

# Search for where PdfPayload is created/returned
rg "PdfPayload\s*[:{]|pages:\s*\[\|pages:" src/tools/ -B 3 -A 3 | head -80

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 58


🏁 Script executed:

# Find PdfPageItem type definition
rg "interface PdfPageItem|type PdfPageItem" src/ -B 2 -A 10

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 633


🏁 Script executed:

# Verify the exact behavior at line 163 - what happens with optional chaining
# Optional chaining: result.payload?.pages will return undefined if payload is undefined
# Then calling .forEach() on undefined should error
python3 << 'EOF'
# Demonstrate the issue
code = "result.payload?.pages.forEach(...)"
print("JavaScript optional chaining behavior:")
print(f"Code: {code}")
print("If result.payload is undefined:")
print("  result.payload?.pages evaluates to undefined")
print("  undefined.forEach(...) throws TypeError")
print("\nThis is the actual bug - optional chaining on property doesn't protect the method call")
EOF

Repository: wonderwhy-er/DesktopCommanderMCP

Length of output: 162


Add optional chaining to PDF page and image iteration to prevent runtime errors.

The current code has a subtle but real bug at line 163:

result.payload?.pages.forEach((page, i) => {
    page.images.forEach((image, i) => {

Optional chaining (?.) only protects property access on the left side. If result.payload is undefined, the expression result.payload?.pages returns undefined, and calling .forEach() on undefined throws a TypeError.

Additionally, while the type system guarantees images is an array when a PdfPageItem exists, defensive programming suggests protecting it as well.

The fix is straightforward—add optional chaining to both .forEach() calls and adjust the content guard to only apply where needed:

-    for (const result of fileResults) {
-        if (!result.error && result.content !== undefined) {
-            if (result.isPdf) {
-                result.payload?.pages.forEach((page, i) => {
-                    page.images.forEach((image, i) => {
+    for (const result of fileResults) {
+        if (!result.error) {
+            if (result.isPdf) {
+                result.payload?.pages?.forEach((page) => {
+                    page.images?.forEach((image) => {
                         contentItems.push({
                             type: "image",
                             data: image.data,
                             mimeType: image.mimeType
                         });
                     });
                     contentItems.push({
                         type: "text",
                         text: page.text,
                     });
                 });
-            } else if (result.isImage && result.mimeType) {
+            } else if (result.isImage && result.mimeType && result.content !== undefined) {
                 // For image files, add an image content item
                 contentItems.push({
                     type: "image",
                     data: result.content,
                     mimeType: result.mimeType
                 });
-            } else {
+            } else if (result.content !== undefined) {
                 // For text files, add a text summary
                 contentItems.push({
                     type: "text",
                     text: `\n--- ${result.path} contents: ---\n${result.content}`
                 });
             }
         }
     }

This removes the global content guard (PDFs don't need content to emit images from payload), adds safe optional chaining on both pages?.forEach() and images?.forEach(), and applies the content check only where it's semantically required.

🤖 Prompt for AI Agents
In src/handlers/filesystem-handlers.ts around lines 140 to 176, the PDF
iteration uses result.payload?.pages.forEach(...) and page.images.forEach(...),
which can throw if pages or images are undefined; update the loops to use
optional chaining (pages?.forEach and images?.forEach) and remove the global
result.content guard for PDF handling so PDFs can emit images/text from payload
even when result.content is undefined; keep the content existence check for
non-PDF branches where actual file content is required.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
src/handlers/filesystem-handlers.ts (1)

166-179: Major: Missing optional chaining on forEach calls can cause runtime errors.

Lines 167-168 still have the bug flagged in the past review: result.payload?.pages.forEach(...) and page.images.forEach(...) will throw a TypeError if pages or images is undefined, because optional chaining (?.) only protects the property access, not the method call.

Apply this diff to add optional chaining on both forEach calls:

             if (result.isPdf) {
-                result.payload?.pages.forEach((page, i) => {
-                    page.images.forEach((image, i) => {
+                result.payload?.pages?.forEach((page) => {
+                    page.images?.forEach((image) => {
                         contentItems.push({
                             type: "image",
                             data: image.data,
                             mimeType: image.mimeType
                         });
                     });
                     contentItems.push({
                         type: "text",
                         text: page.text,
                     });
                 });

Based on the past review comment.

src/tools/pdf/markdown.ts (1)

18-21: Import PageRange from pdf2md instead of re‑declaring it

PageRange is already defined and exported in src/tools/pdf/lib/pdf2md.ts. Duplicating the type here risks the two definitions drifting over time and breaks the “single source of truth” for the paging contract. Better to import the shared type and delete this local alias.

You can do something like:

-import { PdfParseResult, pdf2md } from './lib/pdf2md.js';
+import type { PdfParseResult, PageRange } from './lib/pdf2md.js';
+import { pdf2md } from './lib/pdf2md.js';
@@
-type PageRange = {
-    offset: number;
-    length: number;
-}

This keeps the runtime import for pdf2md intact while reusing the canonical PageRange type.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0906011 and 877109c.

📒 Files selected for processing (4)
  • src/handlers/filesystem-handlers.ts (10 hunks)
  • src/tools/mime-types.ts (2 hunks)
  • src/tools/pdf/markdown.ts (1 hunks)
  • test/test-pdf-parsing.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/tools/mime-types.ts
  • test/test-pdf-parsing.js
🧰 Additional context used
🧬 Code graph analysis (2)
src/tools/pdf/markdown.ts (2)
src/tools/pdf/lib/pdf2md.ts (3)
  • PageRange (59-62)
  • PdfParseResult (35-38)
  • pdf2md (70-123)
test/test-pdf-parsing.js (1)
  • markdown (49-49)
src/handlers/filesystem-handlers.ts (4)
src/tools/filesystem.ts (1)
  • writePdf (1178-1232)
src/types.ts (1)
  • ServerResult (73-77)
src/tools/schemas.ts (1)
  • WritePdfArgsSchema (78-99)
src/error-handlers.ts (1)
  • createErrorResponse (9-17)
🪛 Biome (2.1.2)
src/tools/pdf/markdown.ts

[error] 34-34: This code will never be reached ...

... because this statement will return from the function beforehand

(lint/correctness/noUnreachable)

🔇 Additional comments (3)
src/handlers/filesystem-handlers.ts (1)

318-330: LGTM! Success message correctly handles optional outputPath.

The handler properly defaults targetPath to the input path when outputPath is not provided, and the success message conditionally includes the original file path. This addresses the past review comment.

src/tools/pdf/markdown.ts (2)

5-15: Helper utilities for URL detection and PDF loading look solid

isUrl and loadPdfToBuffer provide a clear split between HTTP(S) sources and local files, and the Buffer | ArrayBuffer return type matches how parsePdfToMarkdown consumes the data. No issues here from a correctness or readability perspective.


42-51: Defensive check is unnecessary—md-to-pdf throws on failure rather than resolving falsy

The md-to-pdf library (v5.2.4) throws errors on failure and resolves to an object with content on success. The current try-catch block already handles this correctly, and the suggested guard clause for !pdf || !pdf.content is not needed. The existing code is robust as-is.

Comment on lines 63 to 76
const offset = parsed.offset ?? 0;
const length = parsed.length ?? defaultLimit;

const fileResult = await readFile(parsed.path, parsed.isUrl, offset, length);

if (fileResult.isPdf) {
const meta = fileResult.payload?.metadata;
const author = meta?.author ? `, Author: ${meta?.author}` : "";
// Use the provided limits or defaults.
// If the caller did not supply an explicit length, fall back to the configured default.
const rawArgs = args as { offset?: number; length?: number } | undefined;
const offset = rawArgs && 'offset' in rawArgs ? parsed.offset : 0;
const length = rawArgs && 'length' in rawArgs ? parsed.length : defaultLimit;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Duplicate variable declarations cause syntax error.

Lines 65-66 declare offset and length, then lines 72-76 redeclare the same variables with different logic. This produces a syntax error—const cannot be redeclared in the same scope.

Lines 72-76 correctly implement the fix from the past review comment (checking raw args to respect the configured default when the caller omits length), so remove lines 65-66.

Apply this diff:

-        // Use the provided limits or defaults
-        const offset = parsed.offset ?? 0;
-        const length = parsed.length ?? defaultLimit;
-
         const fileResult = await readFile(parsed.path, parsed.isUrl, offset, length);
         if (fileResult.isPdf) {
             const meta = fileResult.payload?.metadata;
             const author = meta?.author ? `, Author: ${meta?.author}` : "";
         // Use the provided limits or defaults.
         // If the caller did not supply an explicit length, fall back to the configured default.
         const rawArgs = args as { offset?: number; length?: number } | undefined;
         const offset = rawArgs && 'offset' in rawArgs ? parsed.offset : 0;
         const length = rawArgs && 'length' in rawArgs ? parsed.length : defaultLimit;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/handlers/filesystem-handlers.ts around lines 65 to 76 there are duplicate
const declarations for `offset` and `length` causing a syntax error; remove the
earlier declarations on lines 65-66 (`const offset = parsed.offset ?? 0; const
length = parsed.length ?? defaultLimit;`) and keep the later logic (lines 72-76)
that inspects rawArgs to correctly fall back to configured defaults when
`length` is omitted, ensuring only one set of `offset`/`length` declarations
exists in the scope.

Comment on lines 27 to 35
export async function parsePdfToMarkdown(source: string, pageNumbers: number[] | PageRange = []): Promise<PdfParseResult> {
try {
const data = await loadPdfToBuffer(source);
const byteData = data instanceof Uint8Array ? data : new Uint8Array(data);

// @ts-ignore: Type definition mismatch for ESM usage
return await pdf2md(byteData, pageNumbers);
return await pdf2md(new Uint8Array(data), pageNumbers);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove unreachable second pdf2md call and redundant conversion

After the first return await pdf2md(byteData, pageNumbers); the function exits, so the subsequent return await pdf2md(new Uint8Array(data), pageNumbers); is unreachable. Biome correctly flags this as an error, and leaving it in can break CI and confuse future readers.

You can simplify to:

-        const byteData = data instanceof Uint8Array ? data : new Uint8Array(data);
-
-        // @ts-ignore: Type definition mismatch for ESM usage
-        return await pdf2md(byteData, pageNumbers);
-        return await pdf2md(new Uint8Array(data), pageNumbers);
+        const byteData = data instanceof Uint8Array ? data : new Uint8Array(data);
+
+        // @ts-ignore: Type definition mismatch for ESM usage
+        return await pdf2md(byteData, pageNumbers);

This keeps the optimized Uint8Array handling and resolves the unreachable-code lint error.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function parsePdfToMarkdown(source: string, pageNumbers: number[] | PageRange = []): Promise<PdfParseResult> {
try {
const data = await loadPdfToBuffer(source);
const byteData = data instanceof Uint8Array ? data : new Uint8Array(data);
// @ts-ignore: Type definition mismatch for ESM usage
return await pdf2md(byteData, pageNumbers);
return await pdf2md(new Uint8Array(data), pageNumbers);
export async function parsePdfToMarkdown(source: string, pageNumbers: number[] | PageRange = []): Promise<PdfParseResult> {
try {
const data = await loadPdfToBuffer(source);
const byteData = data instanceof Uint8Array ? data : new Uint8Array(data);
// @ts-ignore: Type definition mismatch for ESM usage
return await pdf2md(byteData, pageNumbers);
🧰 Tools
🪛 Biome (2.1.2)

[error] 34-34: This code will never be reached ...

... because this statement will return from the function beforehand

(lint/correctness/noUnreachable)

🤖 Prompt for AI Agents
In src/tools/pdf/markdown.ts around lines 27 to 35, remove the unreachable
second return so the function only calls pdf2md once with the already-normalized
Uint8Array; keep the existing byteData conversion (const byteData = data
instanceof Uint8Array ? data : new Uint8Array(data)) and replace the two return
statements with a single return await pdf2md(byteData, pageNumbers) so there is
no unreachable code or redundant conversion.

wonderwhy-er and others added 6 commits December 1, 2025 14:12
- Import PageRange type from canonical location ./lib/pdf2md.js
- Remove duplicate local PageRange type definition in markdown.ts
- Add missing title variable in filesystem-handlers.ts that was causing build failure
@codeant-ai
Copy link
Contributor

codeant-ai bot commented Dec 2, 2025

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai bot added size:XXL This PR changes 1000+ lines, ignoring generated files and removed size:XXL This PR changes 1000+ lines, ignoring generated files labels Dec 2, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
test/test-pdf-creation.js (1)

161-161: Entry point detection is unreliable across platforms.

The comparison import.meta.url === file://${process.argv[1]}`` fails when process.argv[1] is a relative path or uses platform-specific separators (e.g., Windows backslashes), preventing `main()` from running when invoked directly.

Apply this diff to use normalized filesystem paths:

-if (import.meta.url === `file://${process.argv[1]}`) {
+if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 307d302 and 1286099.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • test/samples/Presentation Example.pdf is excluded by !**/*.pdf
📒 Files selected for processing (4)
  • package.json (2 hunks)
  • src/tools/pdf/manipulations.ts (1 hunks)
  • src/tools/pdf/markdown.ts (1 hunks)
  • test/test-pdf-creation.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • package.json
  • src/tools/pdf/markdown.ts
  • src/tools/pdf/manipulations.ts
🧰 Additional context used
🧬 Code graph analysis (1)
test/test-pdf-creation.js (2)
test/test-pdf-parsing.js (2)
  • __filename (13-13)
  • markdown (49-49)
src/tools/filesystem.ts (1)
  • writePdf (1178-1232)

Comment on lines 140 to 158
// --- Modification Test ---
console.log('\n2. Testing PDF Modification - keep layout...');

await writePdf(SAMPLE_FILE, [
{
type: 'insert',
pageIndex: 0,
markdown: '# New Cover Page\n\nThis page was inserted dynamically.\n\n## Summary\nWe deleted the original pages and added this one.'
},
{
type: 'insert',
pageIndex: 1,
markdown: '# Appendix\n\nThis page was appended to the end.'
}

], SAMPLE_FILE_MODIFIED);

console.log('✅ PDF modified successfully');
console.log(` Saved to: ${SAMPLE_FILE_MODIFIED}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add verification for the second modification test.

The second modification test (lines 143-155) creates SAMPLE_FILE_MODIFIED but doesn't verify the output, unlike the first modification test which checks file stats and size (lines 123-130). Without verification, this test could silently pass even if the output is empty or invalid.

Add verification after line 155:

 ], SAMPLE_FILE_MODIFIED);
 
 console.log('✅ PDF modified successfully');
 console.log(`   Saved to: ${SAMPLE_FILE_MODIFIED}`);
+
+const sampleModStats = await fs.stat(SAMPLE_FILE_MODIFIED);
+if (sampleModStats.size > 0) {
+    console.log('✅ Modified sample PDF is valid (non-empty)');
+    console.log(`   Modified File Size: ${sampleModStats.size} bytes`);
+} else {
+    console.error('❌ Modified sample PDF file is empty');
+    process.exit(1);
+}
🤖 Prompt for AI Agents
In test/test-pdf-creation.js around lines 140 to 158, the second modification
test writes SAMPLE_FILE_MODIFIED but does not verify the output; add the same
verification steps used in the first modification test: after the writePdf
completes, check that SAMPLE_FILE_MODIFIED exists, call fs.statSync on it,
assert the file size is greater than 0 (and optionally compare size to the
original SAMPLE_FILE or ensure it changed), and log the success with the saved
path; place these checks immediately after line 155 so the test fails if the
modified PDF is empty or missing.

@codeant-ai
Copy link
Contributor

codeant-ai bot commented Dec 2, 2025

CodeAnt AI Incremental review completed.

wonderwhy-er and others added 2 commits December 2, 2025 13:50
Co-authored-by: codeant-ai[bot] <151821869+codeant-ai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/tools/pdf/manipulations.ts (1)

111-111: Fix logic bug: empty markdown strings are incorrectly rejected.

The check if (op.markdown) treats empty strings as falsy, causing valid empty markdown to skip this branch and throw "No source provided" or fall through incorrectly. Since PdfInsertOperationSchema allows empty strings (z.string().optional() with no .min(1)), this is a schema-runtime mismatch.

Apply this diff to fix:

-            if (op.markdown !== undefined) {
+            if (op.markdown !== undefined) {

Based on past review comment highlighting the same issue.

🧹 Nitpick comments (1)
src/tools/pdf/manipulations.ts (1)

109-109: Use strict equality for type comparison.

Replace loose equality == with strict equality === for consistency with TypeScript best practices.

Apply this diff:

-        else if (op.type == 'insert') {
+        else if (op.type === 'insert') {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1286099 and dcd643c.

📒 Files selected for processing (2)
  • src/tools/pdf/manipulations.ts (1 hunks)
  • test/test-pdf-creation.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/test-pdf-creation.js
🧰 Additional context used
🧬 Code graph analysis (1)
src/tools/pdf/manipulations.ts (4)
src/tools/pdf/index.ts (5)
  • PdfInsertOperation (2-2)
  • PdfDeleteOperation (2-2)
  • PdfOperations (2-2)
  • editPdf (1-1)
  • parseMarkdownToPdf (3-3)
src/tools/schemas.ts (3)
  • PdfInsertOperationSchema (63-69)
  • PdfDeleteOperationSchema (71-74)
  • PdfOperationSchema (76-76)
src/tools/pdf/utils.ts (1)
  • normalizePageIndexes (5-12)
src/tools/pdf/markdown.ts (1)
  • parseMarkdownToPdf (36-46)
🔇 Additional comments (5)
src/tools/pdf/manipulations.ts (5)

8-13: LGTM: Clean type inference from Zod schemas.

Using z.infer to derive TypeScript types from Zod schemas ensures type consistency between runtime validation and compile-time checking.


15-19: LGTM: Flexible PDF loading with proper type handling.

The function correctly handles both file paths and buffer inputs, with appropriate error propagation.


26-37: LGTM: Correct page deletion with descending sort.

The descending sort on line 30 correctly prevents index-shifting issues during sequential page removal.


39-72: LGTM: Comprehensive page layout extraction.

The margin calculation and point-to-inch conversion logic is sound. Note: The comment on line 58 states "doesn't accept decimal points" but toFixed(4) produces decimals; if this works correctly in practice, consider updating the comment for clarity.


74-88: LGTM: Proper page insertion with correct bounds validation.

The bounds check correctly allows insertion at getPageCount() (appending), and sequential insertion increments the position appropriately.

@wonderwhy-er wonderwhy-er merged commit 6b3b7e5 into main Dec 2, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants